﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;

using NUnit.Framework;

using Supremacy.Types;

using System.Linq;

namespace Supremacy.Tests.SupremacyCore.Types
{
    /// <summary>
    /// Unit tests for Supremacy.Types.DistribugionGroup<T>
    /// </summary>
    [TestFixture]
    public class DistributionGroupTest
    {
        /// <summary>
        /// Return an integer group instance with a specified number of distribution members
        /// </summary>
        /// <param name="instanceCount">number of instances to create for the returned group.</param>
        /// <returns>DistributionGroup(int)</returns>
        public DistributionGroup<int> DistributionGroupTestInstance(int instanceCount)
        {
            var ItemList = new List<int>();
            for (int i = 1; i <= instanceCount; i++)
            {
                ItemList.Add(i);
            }

            var Group = new DistributionGroup<int>(ItemList);

            return Group;
        }

        /// <summary>
        /// For a given group check that required event handles on PropertyChanged, ValueChanged are set
        /// Note: This method could be extended to check that a method in GetInvocationList is actually the correct method from the group
        /// </summary>
        public void Constructor_HandlersSet(DistributionGroup<int> Group)
        {
            foreach (var distribution in Group.Children)
            {
                Type type = distribution.GetType();

                // We don't really need to hook up to the PropertyChanged event, because all the handler does is raise PropertyChanged on the
                // parent group for the 'Values' property, which can be done by hooking up to just the ValueChanged property on the children.

                //var PropertyChangedDelegate =
                //    (MulticastDelegate)
                //    type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(
                //        distribution);
                //// Check that the number of methods == 1!
                //Assert.IsNotNull(PropertyChangedDelegate, "Property Changed Event is not set");
                //Assert.AreEqual(1, PropertyChangedDelegate.GetInvocationList().Length, "Property Changed event not set");

                var ValueChangedDelegate =
                    (MulticastDelegate)
                    type.GetField("ValueChanged", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(distribution);
                Assert.IsNotNull(ValueChangedDelegate, "Value Changed event not set");
                Assert.AreEqual(1, ValueChangedDelegate.GetInvocationList().Length, "Value Changed Property not set");
            }
        }

        /// <summary>
        /// Check <see cref="ArgumentNullException"/> is raised when attemping to create a group with no initial values
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Constructor_InitialValuesNull()
        {
            new DistributionGroup<object>(null);
        }

        /// <summary>
        /// Test that for each distribution in the group we set the correct event handlers
        /// </summary>
        [Test]
        public void Constuctor_HandlersSet()
        {
            Constructor_HandlersSet(DistributionGroupTestInstance(2));
        }

        /// <summary>
        /// Check that the return value for a distribution maches the expected. 
        /// </summary>
        [Test]
        public void DistributionValueCollectionCheckValue()
        {
            DistributionGroup<int> Group = DistributionGroupTestInstance(2);

            Group.TotalValue = 10;

            IEnumerator<Distribution<int>> Enumerator = Group.Children.GetEnumerator();
            Enumerator.MoveNext();
            Enumerator.Current.Value = 0.2f;

            Enumerator.MoveNext();
            Enumerator.Current.Value = 0.8f;

            Assert.AreEqual(2, Group.Values[1]);
            Assert.AreEqual(8, Group.Values[2]);
        }

        /// <summary>
        /// Check that if a Property or Value is modified in a distribution an approprate event is correctly raised at the group
        /// </summary>
        [Test]
        public void PropertyChanged_isRaisedOnChildModified()
        {
            DistributionGroup<int> Group = DistributionGroupTestInstance(2);
            var Listener = new Listener<PropertyChangedEventArgs>();

            Group.PropertyChanged += Listener.Listen;

            //Our Group has two instances
            IEnumerator<Distribution<int>> Enumerator = Group.Children.GetEnumerator();
            Enumerator.MoveNext();
            Enumerator.Current.Value = 0.4f;

            Assert.GreaterOrEqual(Listener.ReceivedEvents.OfType<PropertyChangedEventArgs>().Where(o => string.Equals(o.PropertyName, "Values", StringComparison.Ordinal)).Count(), 2  );
        }

        /// <summary>
        /// Test that serializing a group works correctly
        /// </summary>
        [Test]
        public void TestSerialization()
        {
            DistributionGroup<int> GroupA = DistributionGroupTestInstance(2);

            var bf = new BinaryFormatter();
            var mem = new MemoryStream();
            bf.Serialize(mem, GroupA);

            string temp = Convert.ToBase64String(mem.ToArray());

            var GroupB = (DistributionGroup<int>)bf.Deserialize(new MemoryStream(Convert.FromBase64String(temp)));

            Assert.AreEqual(GroupA.TotalValue, GroupB.TotalValue);
            Assert.AreEqual(GroupA.Children.Count, GroupB.Children.Count);
            Assert.AreEqual(GroupA._lastChangeIndex, GroupB._lastChangeIndex);

            Constructor_HandlersSet(GroupB);
        }

        /// <summary>
        /// Test that that the set property equals the actual
        /// </summary>
        [Test]
        public void TotalValue_isPropertySet()
        {
            DistributionGroup<int> Group = DistributionGroupTestInstance(2);
            Group.TotalValue = 1;
            Assert.AreEqual(1, Group.TotalValue, "Total Value Property not correctly set");
        }

        /// <summary>
        /// Check an event is raised when modifying the total value
        /// </summary>
        [Test]
        public void TotalValue_PropertyChanged()
        {
            DistributionGroup<int> Group = DistributionGroupTestInstance(2);
            var Listener = new Listener<PropertyChangedEventArgs>();

            Group.PropertyChanged += Listener.Listen;

            Group.TotalValue = 0;

            Assert.AreEqual(1, Listener.ReceivedEvents.Count, "Incorrect number of events received");
        }

        /// <summary>
        /// Check <see cref="ArgumentNullException"/> is raised when setting a negative value
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentException))]
        public void TotalValue_SetNegative()
        {
            DistributionGroup<int> Group = DistributionGroupTestInstance(2);
            Group.TotalValue = -1;
        }
    }

    /// <summary>
    /// Unit test for Supremacy.Types.DistributionEventArgs<TKey>
    /// </summary>
    [TestFixture]
    public class DistributionEventArgsTest
    {
        [Test]
        public void RaiseHandleTest()
        {
            var List = new List<object>
                       {
                           new object(),
                           new object()
                       };

            var test = new DistributionGroup<object>(List);
            var listener = new Listener<DistributionEventArgs<object>>();

            foreach (var distribution in test.Children)
            {
                distribution.ValueChanged += listener.Listen;
            }

            IEnumerator<Distribution<object>> Enumerator = test.Children.GetEnumerator();
            Enumerator.MoveNext();
            Enumerator.Current.Value = 0.4f;

            //Asert that we have raised at least one value changed event
            Assert.GreaterOrEqual(listener.ReceivedEvents.Count, 1, "Incorrect number of events received");
        }
    } ;

    /// <summary>
    /// Unit tests for Supremacy.Types.Distribution(T)
    /// </summary>
    [TestFixture]
    public class DistributionTest
    {
        /// <summary>
        /// Gets a Distribution instance using the minimal constructors for Distribution and <see cref="DistributionGroup{TKey}"/>.
        /// </summary>
        /// <typeparam name="T">Type for Distribution.</typeparam>
        /// <returns>Distribution(T)</returns>
        private static Distribution<T> GetDefaultDistribution<T>(T key)
        {
            return new DistributionGroup<T>(key)[key];
        }

        /// <summary>
        /// Gets a DistributionGroup instance with two <see cref="Distribution&lt;TKey&gt;"/>s.
        /// </summary>
        /// <typeparam name="T">Type for Distribution.</typeparam>
        /// <returns>DistributionGroup&lt;T&gt;</returns>
        private static DistributionGroup<T> GetTwoItemDistributionGroup<T>(T key1, T key2)
        {
            return new DistributionGroup<T>(key1, key2);
        }

        /// <summary>
        /// Tests whether an <see cref="ArgumentNullException"/> is raised when the constructor is
        /// presented with a null parameter.
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void ConstructorNullArgumentTest()
        {
            Assert.IsNull(new Distribution<object>(null));
        }

        /// <summary>
        /// Tests whether the '_group' field is initialised correctly by the constructor
        /// </summary>
        [Test]
        public void ConstructorTest()
        {
            var expected = new DistributionGroup<object>();
            var distribution = new Distribution<object>(expected);

            object actual = distribution.GetType().GetField(
                "_group",
                BindingFlags.Instance |
                BindingFlags.GetField |
                BindingFlags.NonPublic).GetValue(distribution);

            Assert.AreSame(expected, actual);
        }

        /// <summary>
        /// Tests whether the 'locked' property is false by default.
        /// </summary>
        [Test]
        public void IsLockedFalseByDefaultTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());

            Assert.IsFalse(distribution.IsLocked);
        }

        /// <summary>
        /// Tests that the get/set methods for the IsLocked property work correctly
        /// and that a PropertyChangedEvent is raised when the value changes when
        /// the property is set to true.
        /// </summary>
        [Test]
        public void IsLockedGetSetLockTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            const bool expected = true;
            distribution.IsLocked = !expected;
            var autoEvent = new AutoResetEvent(false);

            distribution.PropertyChanged += delegate(object o, PropertyChangedEventArgs e)
                                            {
                                                if (e.PropertyName == "IsLocked")
                                                {
                                                    Assert.AreSame(distribution, o);
                                                    Assert.AreEqual(expected, distribution.IsLocked);
                                                    autoEvent.Set();
                                                }
                                            };

            distribution.IsLocked = expected;
            autoEvent.WaitOne(new TimeSpan(0, 0, 10), false);
        }

        /// <summary>
        /// Tests that the get/set methods for the IsLocked property work correctly
        /// and that a PropertyChangedEvent is raised when the value changes when
        /// the property is set to false.
        /// </summary>
        [Test]
        public void IsLockedGetSetUnlockTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            const bool expected = false;
            distribution.IsLocked = !expected;
            var autoEvent = new AutoResetEvent(false);

            distribution.PropertyChanged += delegate(object o, PropertyChangedEventArgs e)
                                            {
                                                if (e.PropertyName == "IsLocked")
                                                {
                                                    Assert.AreSame(distribution, o);
                                                    Assert.AreEqual(expected, distribution.IsLocked);
                                                    autoEvent.Set();
                                                }
                                            };

            distribution.IsLocked = expected;
            autoEvent.WaitOne(10000, false);
        }

        /// <summary>
        /// Passes if setting the value of IsLocked to its current value
        /// does not cause a PropertyChangedEvent to be raised.
        /// </summary>
        [Test]
        public void IsLockedPropertyChangedDoubleRaiseTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            const bool expected = true;
            distribution.IsLocked = expected;

            distribution.PropertyChanged += delegate(object o, PropertyChangedEventArgs e)
                                            {
                                                if (e.PropertyName == "IsLocked")
                                                    Assert.Fail(
                                                        "Assigning previous value to IsLocked caused a PropertyChangedEvent to be raised inappropriately");
                                            };

            distribution.IsLocked = distribution.IsLocked;
        }

        /// <summary>
        /// Passes is <see cref="ArgumentOutOfRangeException"/> is raised when SetValue is invoked
        /// with an argument greater than Distribution.MaxValue
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentException))]
        public void SetValueAboveMaxValueTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            var expected = new Percentage(double.MaxValue);

            try
            {
                distribution.GetType().GetMethod(
                    "SetValue",
                    BindingFlags.Instance |
                    BindingFlags.NonPublic |
                    BindingFlags.InvokeMethod)
                    .Invoke(distribution, new object[] { expected });
            }
            catch (TargetInvocationException ex)
            {
                throw ex.InnerException;
            }
        }

        /// <summary>
        /// Passes if <see cref="ArgumentOutOfRangeException"/> is raised when SetValue is invoked
        /// with an argument less than Distribution.MinValue
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentOutOfRangeException))]
        public void SetValueBelowMinValueTest()
        {
            // The argument Percentage can have a value < Distribution.MinValue 
            // only if Percentage.MinValue > Distribution<T>.MinValue. Therefore,
            // if this condition is not met, force the test to pass by throwing
            // an ArgumentOutOfRangeException
            if (!(Percentage.MinValue > Distribution<object>.MinValue))
                throw new ArgumentOutOfRangeException();

            Distribution<object> distribution = GetDefaultDistribution(new object());
            var expected = new Percentage(double.MinValue);

            try
            {
                distribution.GetType().GetMethod(
                    "SetValue",
                    BindingFlags.Instance |
                    BindingFlags.NonPublic |
                    BindingFlags.InvokeMethod)
                    .Invoke(distribution, new object[] { expected });
            }
            catch (TargetInvocationException ex)
            {
                throw ex.InnerException;
            }
        }

        /// <summary>
        /// Passes if 'false' is returned when SetValue is invoked on a Distribution with
        /// Value == Distribution.MaxValue and 1 or fewer items in its <see cref="DistributionGroup&lt;T&gt;"/>
        /// </summary>
        [Test]
        public void SetValueGroupCountLeOneMaxValueTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            distribution.Value = Distribution<object>.MaxValue;
            var expected = new Percentage();

            var ret = (bool)distribution.GetType().GetMethod(
                                "SetValue",
                                BindingFlags.Instance |
                                BindingFlags.NonPublic |
                                BindingFlags.InvokeMethod)
                                .Invoke(distribution, new object[] { expected });

            Assert.IsFalse(ret);
        }

        /// <summary>
        /// Passes if 'true' is returned and Value == Distribution.MaxValue 
        /// when SetValue is invoked on a Distribution with 
        /// Value != Distribution.MaxValue and 1 or fewer items in its 
        /// <see cref="DistributionGroup&lt;T&gt;"/>
        /// </summary>
        [Test]
        public void SetValueGroupCountLeOneNotMaxValueTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            Percentage expected = 0.5f;

            var ret = (bool)distribution.GetType().GetMethod(
                                "SetValue",
                                BindingFlags.Instance |
                                BindingFlags.NonPublic |
                                BindingFlags.InvokeMethod)
                                .Invoke(distribution, new object[] { expected });

            Assert.IsFalse(ret);
            Assert.AreEqual(Distribution<object>.MaxValue, distribution.Value);
        }

        /// <summary>
        /// Passes if Value property is set correctly
        /// </summary>
        [Test]
        public void SetValueInternalTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            var expected = new Percentage();

            distribution.SetValueInternal(expected);

            Assert.AreEqual(expected, distribution.Value);
        }

        // SETVALUE
        // SetValue returns 'true' if the value has changed, false otherwise.
        /// <summary>
        /// Passes if <see cref="InvalidOperationException"/> is thrown if the IsLocked is true 
        /// when SetValue is invoked
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void SetValueIsLockedTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            distribution.IsLocked = true;

            // try/catch block required to access the InnerException property - 
            // if an exception is thrown in the target method, it will
            // be wrapped up in a System.Reflection.TargetInvocationException
            try
            {
                distribution.GetType().GetMethod(
                    "SetValue",
                    BindingFlags.Instance |
                    BindingFlags.NonPublic |
                    BindingFlags.InvokeMethod)
                    .Invoke(distribution, new object[] { new Percentage() });
            }
            catch (TargetInvocationException ex)
            {
                throw ex.InnerException;
            }
        }

        /// <summary>
        /// Passes is <see cref="ArgumentException"/> is raised when SetValue is invoked
        /// with a NaN as its argument
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentException))]
        public void SetValueNaNTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            var expected = new Percentage(double.NaN);

            try
            {
                distribution.GetType().GetMethod(
                    "SetValue",
                    BindingFlags.Instance |
                    BindingFlags.NonPublic |
                    BindingFlags.InvokeMethod)
                    .Invoke(distribution, new object[] { expected });
            }
            catch (TargetInvocationException ex)
            {
                throw ex.InnerException;
            }
        }

        /// <summary>
        /// Passes if 'false' is returned when SetValue is invoked with current
        /// value as argument
        /// </summary>
        [Test]
        public void SetValueNoChangeTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            var expected = new Percentage(1.0f);

            distribution.GetType().GetMethod(
                "SetValue",
                BindingFlags.Instance |
                BindingFlags.NonPublic |
                BindingFlags.InvokeMethod)
                .Invoke(distribution, new object[] { expected });

            var ret = (bool)distribution.GetType().GetMethod(
                                "SetValue",
                                BindingFlags.Instance |
                                BindingFlags.NonPublic |
                                BindingFlags.InvokeMethod)
                                .Invoke(distribution, new object[] { expected });

            Assert.IsFalse(ret);
        }

        /// <summary>
        /// Tests whether get/set methods for Value execute correctly and that a PropertyChangedEvent
        /// is raised.
        /// </summary>
        [Test]
        public void ValueGetSetTest()
        {
            var key1 = new object();
            var key2 = new object();
            var group = new DistributionGroup<object>(key1, key2);
            var expected = new Percentage(0.75f);
            var autoEvent = new AutoResetEvent(false);

            group[key1].PropertyChanged += delegate(object o, PropertyChangedEventArgs e)
                                           {
                                               if (String.Equals(
                                                   e.PropertyName, "Value", StringComparison.Ordinal))
                                               {
                                                   autoEvent.Set();
                                                   Assert.AreSame(group[key1], o);
                                                   Assert.AreEqual(expected, group[key1].Value);
                                                   Assert.AreEqual(new Percentage(1.0f - expected), group[key2].Value);
                                                   return;
                                               }
                                           };

            group[key1].Value = expected;
            if (!autoEvent.WaitOne(new TimeSpan(0, 0, 10), false))
                Assert.Fail("ValueChanged event was not raised.");
        }

        /// <summary>
        /// Tests whether ValueChanged event is thrown when the value
        /// of the Value property changes. Implicitly tests 
        /// private method 'OnValueChanged()'
        /// </summary>
        [Test]
        public void ValueOnChangeTest()
        {
            var key1 = new object();
            var key2 = new object();
            DistributionGroup<object> group = GetTwoItemDistributionGroup(key1, key2);
            var autoEvent = new AutoResetEvent(false);

            group[key1].ValueChanged += delegate(object o, DistributionEventArgs<object> e)
                                        {
                                            Assert.AreSame(group[key1], o);
                                            Assert.AreSame(group[key1], e.Source);
                                            autoEvent.Set();
                                            return;
                                        };

            group[key1].Value = new Percentage(0.25f);
            if (!autoEvent.WaitOne(new TimeSpan(0, 0, 10), false))
                Assert.Fail("ValueChanged event was not raised.");
        }

        /// <summary>
        /// Passes if setting the value of Value to its current value
        /// does not cause a PropertyChangedEvent or a 
        /// ValueChanged event to be raised.
        /// </summary>
        [Test]
        public void ValuePropertyChangedDoubleRaiseTest()
        {
            Distribution<object> distribution = GetDefaultDistribution(new object());
            var expected = new Percentage();
            distribution.Value = expected;

            distribution.PropertyChanged += delegate(object o, PropertyChangedEventArgs e)
                                            {
                                                if (e.PropertyName == "IsLocked")
                                                    Assert.Fail(
                                                        "Assigning previous value to Value caused a PropertyChangedEvent to be raised inappropriately");
                                            };

            distribution.ValueChanged +=
                delegate { Assert.Fail("Assign previous value to Value caused a ValueChangedEvent to be raised inappropriately"); };

            distribution.Value = distribution.Value;
        }

        //if (_group._lastChangeIndex == otherValues.IndexOf(this))
        //{
        //    if (this == otherValues[otherValues.Count - 1])
        //    {
        //        _group._lastChangeIndex = 0;
        //    }
        //    else
        //    {
        //        _group._lastChangeIndex = otherValues.IndexOf(this) + 1;
        //    }
        //}
    }
}